Skip to content

feat(backend): centralize amount and currency validation across API and service layers - #139

Open
woahwhattheheck wants to merge 18 commits into
RemitFlow:mainfrom
woahwhattheheck:wire/rfb-130-currency-policy
Open

woahwhattheheck wants to merge 18 commits into
RemitFlow:mainfrom
woahwhattheheck:wire/rfb-130-currency-policy

Conversation

@woahwhattheheck

Copy link
Copy Markdown

Summary

Closes #130.

Introduces a single currency policy shared by API validators and settlement so preview quotes and executed transfers always agree on precision, supported codes, and overflow bounds.

What

  • src/utils/currencyPolicy.js — canonical metadata (ISO minor units, min amount, optional max) for every rate-listed currency; canonicalizeAmount / validateTransferPair / roundToCurrency.
  • Transfer and quote validators call the same policy (transfer enforces max; quote does not).
  • quoteService / rateService.convert round with the destination currency's minor units (JPY stays whole).
  • Unsupported currencies and sub-minor-unit amounts fail before any store mutation.

Why

Different layers previously accepted different precision or unsupported codes, so a quote could succeed and settlement later fail (or vice versa). One policy table keeps preview and execution identical.

How tested

  • npm test — 275 passing (includes new test/currencyPolicy.test.js).
  • Coverage: currency matrix, JPY zero-decimal, overflow, transfer max, preview/settlement contract agreement, HTTP 400 for unsupported codes.

Design tradeoffs

  • Policy table must stay aligned with SUPPORTED_CURRENCIES (boot-time assert). Adding a corridor means updating both the rate table and metadata in one change.
  • Quotes skip the transfer max so informational previews still work for large amounts; create-transfer still enforces it.

woahwhattheheck and others added 18 commits September 24, 2026 15:43
Add a shared currency policy (ISO minor units, min/max, supported codes)
used by quote/transfer validators and settlement so preview amounts match
executed transfers, unsupported currencies fail before mutation, and
zero-decimal corridors like JPY reject fractional sends.

Closes RemitFlow#130.
Keep fee components unrounded until their sum reaches the shared currency
policy. Compensate for floating-point drift at half-minor-unit boundaries
so JPY fees include the flat component without regressing USD cent ties.

Add literal boundary regressions and exercise the HTTP quote/transfer path
for 30 JPY: fee 1 JPY and recipient amount 0.19 USD.

Validation on Node 24.19.0: focused checks 53/53; npm test 289/289.
Malformed JSON and parsed query objects could throw from Number() or an unsupported-currency error label when toString was not callable. Seven actual GET quote / POST transfer cases returned 500 instead of normal validation errors.

Accept only numbers and strings for amount conversion, and describe invalid currency types without invoking their conversion properties. Use the shared currency label in policy, quote, and rate errors. Preserve the previous source ceiling, minor-unit, combined-fee, positive-payout, preview/execution, and idempotency behavior.

Validation: npm test passed 301/301 with zero failures or skips. The existing currency-policy file passed 45/45; six new regressions fail on sole parent e79ead3 while its previous 39 checks pass. Four changed JavaScript files passed syntax checks and git diff --check passed. Node 24.19.0 reused retained dependencies matching all 76 lockfile package versions; no install or dependency change.

Actual local Express HTTP replay on the documented mock FX/Stellar and in-memory store changes all seven malformed JSON/query cases from 500 to 400 with field validation errors and no transfer or retry-key records. Valid transfer 201 and ordinary invalid-amount 400 controls remain. Extended HTTP regressions observe zero settlement calls, transfer/index/audit entries and idempotency records before rejection, and a corrected same-key request then succeeds with the previewed amounts. No live payment or hosted Node 22 CI result is claimed.
Replace the README's all-currencies two-decimal instruction with the
implemented JPY and two-decimal currency rules. Document the shared
source-amount ceiling and the post-fee, destination-rounded payout check.

README only; existing implementation, tests and historical validation
remain unchanged. Checked against current source and existing cases;
no runtime or maintained suite replayed for this documentation update.
Use exact decimal ratios for fees, net amounts and FX conversion before
rounding once to currency minor units. Keep numeric API results and the
existing negative-tie direction. Add focused midpoint and preview/transfer
regressions to the existing currency policy tests.

Native conversion, quote, createTransfer and stored transfer reproduction:
16.04 GBP -> USD: 19.68 -> 19.69
4.14 GBP -> EUR: 4.44 -> 4.45
384 JPY -> EUR: 2.34 -> 2.35
Nine tie/sign controls, twelve fee cases and six existing boundary/rejection
controls pass. Real business/store/idempotency/audit modules ran against
the repository mock Stellar adapter. No live payment was attempted.

Node 24.19.0 with documented default configuration; dotenv loading was a
declared no-op and UUID came from the existing runtime. The maintained HTTP
file is extended but unexecuted here because locked Express 4/dotenv 16
dependencies are unavailable. No full-suite or hosted CI result is claimed.
Link the focused Node 22 run against the immutable rounding source.
All 49 policy cases passed, including the HTTP quote/transfer regressions.
Document the command and limit this evidence to the focused test file.
Explicit TRANSFER_FEE_PERCENT=0 or TRANSFER_FEE_FLAT=0 was treated as
missing and replaced with the normal 1.5 percent / 0.30 defaults. Parse
the fee components with a NaN fallback so individual or complete fee
waivers reach the existing canonical quote calculation. Other numeric
configuration and decimal rounding are unchanged.

Extend the maintained config test file with default, blank, invalid,
zero-percent, zero-flat, both-zero and custom-fee quote cases. The same
11 cases on Node 24.19 gave 8 pass / 3 fail against the previous source
and 11 pass / 0 fail with this change. Local execution supplied process.env
and replaced only dotenv's optional .env loading; configuration, currency
and quote modules executed as published. No full HTTP suite, locked
dependency run or current CI success is claimed.

With both components zero, a 100 USD to EUR quote now has fee 0 and
receiveAmount 92.59, instead of fee 1.80 and receiveAmount 90.93.

Refs RemitFlow#130
… ci]

Integrate d31a56e without replacing the concurrent zero-fee configuration
repair at 452a512. The three readback files are byte-identical to the
executed candidate; configuration and its tests are preserved.

A safe integer number of cents can still change when divided into the
existing Number-valued API. Compare the result's decimal representation
with the exact rounded integer, rejecting only lossy values. Canonical
validation returns the existing numeric-range error instead of throwing.
For example, 90071992547409.91 USD no longer silently becomes
90071992547409.9; representable neighbors and whole-number JPY survive.

Actual Node 22.16.0 maintained tests in run 37191802959:
- Four new boundary groups on parent 2e815f6: 1 pass, 3 fail.
- test/currencyPolicy.test.js on d31a56e: 53 pass, 0 fail, 0 skipped.
- Includes real Express HTTP 400 and no transfer, idempotency, audit or
  payment-submission effects. Runtime dependencies installed from the lock.
No full-suite or live-payment execution is asserted. The later zero-fee
configuration change was not part of that focused run.

Evidence artifact 11298779083 SHA256:
1e3d5120092abb08f55b003cc60e8a12a5f5cab65aa6763efd40e944522325bf
https://github.com/woahwhattheheck/RemitFlow-Backend/actions/runs/37191802959

Refs RemitFlow#130
Document zero-valued fee components and the focused 60-case run against
product 452a512 with locked dependencies. The documented source and
selection make the boundary distinct from historical full-suite claims.
Keep the full computed dimensionless FX ratio in getPair and getQuote rather than rounding it like a currency amount. For NGN/USD, the old responses advertised rate 0 beside a positive 6.40 USD receive amount; they now advertise 0.00065. Fee and receive-amount rounding remain unchanged.

Focused native Node 22.16.0 comparison on complete source 2e815f6 with these two service changes: test/exchangeRatePrecision.test.js went from 1 passed / 2 failed to 3 passed / 0 failed. The supported 72-pair matrix retains the conversion ratio after JSON serialization; separately compared full pair/quote records preserve every non-rate value. No dependency or test-runner changes.

Reproduction with installed project dependencies: node --test test/exchangeRatePrecision.test.js. This local execution used an external inert dotenv.config preload because dotenv was unavailable; real config defaults, services, helpers and static rates executed. No .env loading, application HTTP, live provider, settlement database or full-suite result is claimed. The preload is not committed.

Composed on current 2f3aca0, preserving four intervening configuration/currency-policy/documentation commits. Native comparison confirms neither service changed in that interval. Existing PR139 and issue130 remain the contribution path; no new claim or award assertion.
Convert safe integer Numbers directly to decimal ratios while preserving fractional, string and unsafe-integer parsing, exact rounding and readback validation. Include paired production-service measurements and a reproducible benchmark.
A source amount accepted by the configured transfer ceiling can overflow the destination currency's minor-unit range during conversion. With MAX_TRANSFER_AMOUNT=1000000000000, a 1000000000000 USD to NGN quote previously raised a plain RangeError and reached the error handler as an internal error. Translate that conversion-range failure into the existing ApiError 400 contract while preserving other exceptions and the exact rounding implementation.

Add one focused regression through the production quote, transfer, error-handler, store, audit and idempotency modules. Rejected overflow makes no settlement call and inserts no records. A corrected 50000 USD request reuses the same key, yields 75768769.23 NGN, and replay retains the same transfer with only one settlement call.

Validation: node --test test/quoteOverflow.test.js. Original quote blob 093b0b1 fails the same regression (exit 1); changed quote blob 1fc5319 passes (exit 0, one case). Node v24.19.0, dotenv 16.6.1 and existing runtime uuid 3.4.0. The shipped settlement adapter is local/mock, and the error handler uses a response sink. Scope is service and error-envelope behavior; no Express HTTP, complete-suite or live-settlement result is asserted.

Continue the original issue RemitFlow#130 contribution and preserve parent 3581b9f and all earlier precision, fee, rate and throughput work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(backend): centralize amount and currency validation across API and service layers

1 participant